import {
    Alias,
    AliasMap,
    Categories,
    CategoryFilterFlags,
    CategoryFlags,
    CategoryMap,
    CellStyle,
    Class,
    Compound,
    Context,
    ContextAlternatives,
    ContextMap,
    Contexts,
    ContextUsageFlags,
    ControlStyle,
    ControlTypes,
    DataSource,
    DataSources,
    DataTypes,
    DisplayOrders,
    ElementTypes,
    Field,
    FontStyle,
    IMDMLabeledObject,
    IMDMObject,
    InterviewModesFlags,
    Label,
    LabelObject,
    Labels,
    Language,
    Languages,
    BaseMDMDocument,
    MDMElement,
    MDMElementInstance,
    MDMElementInstances,
    MDMElements,
    MDMFactory,
    MDMField,
    MDMListOf,
    Note,
    Notes,
    ObjectFlags,
    Page,
    Pages,
    Properties,
    Property,
    PropertyValue,
    Range,
    Routing,
    RoutingItem,
    Routings,
    SaveLog,
    Script,
    Scripts,
    ScriptType,
    SourceTypes,
    Style,
    SubAlias,
    Template,
    Templates,
    Types,
    User,
    Variable,
    VariableInstance,
    Variables,
    VariableUsageFlags,
    Version,
    Versions,
    ContextMapItem
} from "./types";
import {
    elementTypeConstantsToTypeName,
    getMDMObjectEntries,
    getUuid,
    isMDMElements,
    isMDMField,
} from "./util";
import { objectAllocator } from "./impl";
import { LocalLanguage, MDMLoaderOptions, Mutable } from "../../common/types";
import { getObjectArrayMapEntries } from "../../common/util";

export function createMDMFactory(options: MDMLoaderOptions): MDMFactory {

    let _options = options;

    let mdmObjectConstructor: new (document: BaseMDMDocument, parent: IMDMObject, flag: ObjectFlags, name: string, uuid?: string) => IMDMObject;
    let mdmListConstructor: new <T>(items?: readonly T[], deleted?: readonly T[]) => MDMListOf<T>;
    let contextMapConstructor: new <T>(items?: readonly ContextMapItem<T>[], deleted?: readonly ContextMapItem<T>[]) => ContextMap<T>;
    let propertiesConstructor: new (document: BaseMDMDocument, id?: string, name?: string, items?: readonly ContextMapItem<Property[]>[]) => Properties;
    let templatesConstructor: new (document: BaseMDMDocument, items?: readonly ContextMapItem<Template[]>[]) => Templates;
    let labelConstructor: new (document: BaseMDMDocument, context: string, items?: readonly LabelObject[]) => Label;
    let labelsConstructor: new (document: BaseMDMDocument, entries?: readonly ContextMapItem<Label>[]) => Labels;
    let typesConstructor: new <T extends IMDMObject>(parent: IMDMObject, uuid?: string, entries?: readonly [string, T][]) => Types<T>;
    let labeledObjectConstructor: new (document: BaseMDMDocument, parent: IMDMObject, flag: ObjectFlags, name: string, uuid?: string, reference?: IMDMLabeledObject) => IMDMLabeledObject;
    let documentConstructor: new () => BaseMDMDocument;

    return {
        createObject,
        createAlias,
        createAliasMap,
        createCategories,
        createCategoryMap,
        createCellStyle,
        createClass,
        createCompound,
        createContext,
        createContextAlternatives,
        createContextMap,
        createContexts,
        createControlStyle,
        createDataSource,
        createDataSources,
        createBaseDocument,
        createField,
        createFontStyle,
        createLabel,
        createLabeledObject,
        createLabelObject,
        createLabels,
        createLanguage,
        createLanguages,
        createMDMElement,
        createMDMElementInstace,
        createMDMElementInstances,
        createMDMElements,
        createMDMField,
        createMDMList,
        createNote,
        createNotes,
        createPage,
        createPages,
        createProperties,
        createProperty,
        createRange,
        createRouting,
        createRoutings,
        createSaveLog,
        createScript,
        createScriptType,
        createStyle,
        createTemplate,
        createTemplates,
        createTypes,
        createUser,
        createVariable,
        createVariableInstance,
        createVariables,
        createVersion,
        createVersions,
    };

    function createObject(document: BaseMDMDocument, parent: IMDMObject, flag: ObjectFlags, name: string, uuid?: string): Mutable<IMDMObject> {
        return new (mdmObjectConstructor || (mdmObjectConstructor = objectAllocator.getMDMObjectConstructor()))(document, parent, flag, name, uuid);
    }

    function createMDMList<T>(items?: readonly T[], deleted?: readonly T[]): MDMListOf<T> {
        return new (mdmListConstructor || (mdmListConstructor = objectAllocator.getMDMListConstructor()))(items, deleted);
    }

    function createContextMap<T>(entries?: readonly ContextMapItem<T>[], deleted?: readonly ContextMapItem<T>[]): ContextMap<T> {
        return new (contextMapConstructor || (contextMapConstructor = objectAllocator.getContextMapConstructor()))(entries, deleted);
    }

    function createDataSource(name: string, uuid: string | undefined, dbLocation: string, cdscName: string, project: string): DataSource {
        return { name, uuid: uuid ?? getUuid(), dbLocation, cdscName, project };
    }

    function createDataSources(defaultObj?: DataSource, items?: readonly DataSource[]): DataSources {
        const obj = (items ?? []) as Mutable<DataSources>;
        obj.default = defaultObj;
        return obj;
    }

    function createVersion(document: BaseMDMDocument, parent: IMDMObject, name: string, dateCreated: string, createdBy: string, description: string = ""): Version {
        const version = createObject(document, parent, ObjectFlags.version, name) as Mutable<Version>;
        version.fullName = name;
        version.dateCreated = dateCreated;
        version.createdBy = createdBy;
        version.isLocked = false;
        version.description = description;
        version.label = "";
        return version;
    }

    function createVersions(document: BaseMDMDocument, parent: IMDMObject, latest?: Version): Versions {
        const versions = createMDMList() as Mutable<Versions>;
        versions.document =document;
        versions.parent = parent;
        versions.name = "";
        versions.objectFlag = ObjectFlags.versions;
        versions.latest = latest;
        versions.uuid = getUuid();
        versions.properties = createProperties(document);
        return versions;
    }

    function createControlStyle(context: string, type?: ControlTypes, readonly?: boolean, accelerator?: string): ControlStyle {
        const obj = {} as Mutable<ControlStyle>;
        obj.context = context;
        obj.type = type;
        obj.readonly = readonly;
        obj.accelerator = accelerator;
        return obj;
    }

    function createFontStyle(context: string, family?: string, size?: number, effects?: number): FontStyle {
        const obj = {} as Mutable<FontStyle>;
        obj.context = context;
        obj.family = family;
        obj.size = size;
        obj.effects = effects;
        return obj;
    }

    function createCellStyle(context: string): Mutable<CellStyle> {
        return { context } as Mutable<CellStyle>;
    }

    function createStyle(context: string): Mutable<Style> {
        return { objectFlag: ObjectFlags.style, context } as Mutable<Style>;
    }

    function createLabelObject(context: string, language: string, text: string, cdata?: boolean): LabelObject {
        return { context, language, text, cdata };
    }

    function createProperty(name: string, context: string, value?: PropertyValue | null, dataSource?: string): Property {
        const obj = { name, context, value, dataSource, versioned: true } as Mutable<Property>;
        if (value !== undefined) {
            if (typeof value === "number") {
                obj.valueType = Number.isInteger(value) ? 3 : 5;
            }
            else if (typeof value === "string") {
                obj.valueType = 8;
            }
            else if (typeof value === "boolean") {
                obj.valueType = 11;
            }
            else {
                obj.valueType = 9;
            }
        }
        return obj;
    }

    function createProperties(document: BaseMDMDocument, id?: string, name?: string, items?: readonly ContextMapItem<Property[]>[]): Properties {
        return new (propertiesConstructor || (propertiesConstructor = objectAllocator.getPropertiesConstructor()))(document, id, name, items);
    }

    function createTemplate(document: BaseMDMDocument, name: string, context: string, value: string): Template {
        return {
            document,
            objectFlag: ObjectFlags.template,
            name,
            context,
            value
        } as Template;
    }

    function createTemplates(document: BaseMDMDocument, items?: readonly ContextMapItem<Template[]>[]): Templates {
        return new (templatesConstructor || (templatesConstructor = objectAllocator.getTemplatiesConstructor()))(document, items);
    }

    function createLabel(document: BaseMDMDocument, context: string, labelObjects?: readonly LabelObject[] | MDMListOf<LabelObject>): Label {
        return new (labelConstructor || (labelConstructor = objectAllocator.getLabelConstructor()))(document, context, labelObjects);
    }

    function createLabels(document: BaseMDMDocument, entries?: readonly ContextMapItem<Label>[]): Labels {
        return new (labelsConstructor || (labelsConstructor = objectAllocator.getLabelsConstructor()))(document, entries);
    }

    function createContextAlternatives(document: BaseMDMDocument, alternatives?: readonly string[]): ContextAlternatives {
        const array = createMDMList(alternatives) as Mutable<ContextAlternatives>;
        array.objectFlag = ObjectFlags.contextAlternatives;
        array.document = document;
        return array;
    }

    function createContext(document: BaseMDMDocument, name: string, usage: ContextUsageFlags = ContextUsageFlags.routings): Context {
        const context = {} as Mutable<Context>;
        context.objectFlag = ObjectFlags.context;
        context.document = document;
        context.name = name;
        context.usage = usage;
        return context;
    }

    function createContexts(document: BaseMDMDocument, base: string, current: string): Contexts {
        const contexts = createMDMList() as Mutable<Contexts>;
        contexts.objectFlag = ObjectFlags.contexts;
        contexts.document = document;
        contexts.base = base;
        contexts.current = current;
        return contexts;
    }

    function createLanguage(document: BaseMDMDocument, language: LocalLanguage, id: string): Language {
        const languageObject = {} as Mutable<Language>;
        languageObject.objectFlag = ObjectFlags.language;
        languageObject.document = document;
        languageObject.id = id;
        languageObject.longName = language.longCode;
        languageObject.shortName = language.shortCode;
        languageObject.properties = createProperties(document);
        return languageObject;
    }

    function createLanguages(document: BaseMDMDocument, base: string, current: string): Languages {
        const array = createMDMList() as Mutable<Languages>;
        array.objectFlag = ObjectFlags.languages;
        array.document = document;
        array.base = base;
        array.current = current;
        array.properties = createProperties(document);
        return array;
    }

    function createLabeledObject(document: BaseMDMDocument, parent: IMDMObject, flag: ObjectFlags, name: string, id?: string, reference?: IMDMLabeledObject): Mutable<IMDMLabeledObject> {
        return new (labeledObjectConstructor || (labeledObjectConstructor = objectAllocator.getLabeledObjectConstructor()))(document, parent, flag, name, id, reference);
    }

    function createCategoryMap(document: BaseMDMDocument, entries?: readonly [string, number][]): CategoryMap {
        const map = {} as Mutable<CategoryMap>;
        map.document = document;
        map.nameToValueMap = new Map<string, number>(entries);
        map.valueToNameMap = new Map<number, string>(entries?.map(e => [e[1], e[0]]));
        return map;
    }

    function createRange(lowerBoundary: number | string, upperBoundary: number | string): Range {
        return { lowerBound: lowerBoundary, upperBound: upperBoundary };
    }

    function createTypes<T extends IMDMObject>(parent: IMDMObject, uuid?: string, entries?: readonly [string, T][]): Types<T> {
        return new (typesConstructor || (typesConstructor = objectAllocator.getTypesConstructor()))(parent, uuid, entries);
    }

    function createUser(name: string, fileVersion: string, comment: string): User {
        return { name, fileVersion, comment };
    }

    function createSaveLog(date: string, versionSet: string, mdmFileVersion: string, userName: string, saveCount: number): SaveLog {
        return { date, versionSet, mdmFileVersion, userName, saveCount, user: createUser(_options.userName, _options.mdmVersion, "Executable of the process MDM run under") };
    }

    function createNote(value: string, name: string, context: string): Note {
        return {
            objectFlag: ObjectFlags.note,
            value,
            name,
            context
        };
    }

    function createNotes(items?: readonly Note[]): Notes {
        const notes = createMDMList(items) as Mutable<Notes>;
        notes.objectFlag = ObjectFlags.notes;
        return notes;
    }

    function createCategories(fullName?: string, value?: number, filterFlag: CategoryFilterFlags = CategoryFilterFlags.all, items?: readonly MDMElement[]): Categories {
        const categories = createMDMList(items) as Mutable<Categories>;
        categories.fullName = fullName ?? "";
        categories.value = value;
        categories.categoryFilter = filterFlag;
        return categories;
    }

    function createMDMElement(document: BaseMDMDocument, parent: IMDMObject, name: string, type: ElementTypes, ownerField: Field | undefined, uuid?: string, categoryFlag: CategoryFlags = CategoryFlags.none): MDMElement {
        const element = createLabeledObject(document, parent, ObjectFlags.element, name, uuid) as Mutable<MDMElement>;
        element.objectFlag = ObjectFlags.element;
        element.flag = categoryFlag;
        element.type = type;
        element.versioned = true;
        element.uuid = uuid ?? getUuid();
        element.document = element.document;
        element.parent = parent;
        element.ownerField = ownerField;
        element.fullName = isMDMElements(parent) && !parent.namespace ? `${parent.fullName}.${name}` : name;
        element.fixed = false;
        element.isDerived = false;
        element.typeName = elementTypeConstantsToTypeName(element.type);
        element.hasNoData = false;
        element.canFilter = true;
        element.expression = undefined;
        element.keyCode = undefined;
        element.factor = undefined;
        element.nv = undefined;
        element.autoElements = undefined;
        element.nativeValue = undefined;
        return element;
    }

    function createMDMElements(document: BaseMDMDocument, parent: IMDMObject, ownerField?: Field, name: string = "", uuid?: string, reference?: MDMElements, items?: readonly IMDMLabeledObject[]): MDMElements {
        const elements = createMDMList(items) as Mutable<MDMElements>;
        elements.objectFlag = ObjectFlags.elements;
        elements.document = document;
        elements.parent = parent;
        elements.name = name;
        elements.fullName = name;
        elements.uuid = uuid ?? getUuid();
        elements.ownerField = ownerField;
        elements.isDerived = false;
        elements.isDBElements = false;
        elements.versioned = true;
        elements.namespace = false;
        elements.displayOrder = DisplayOrders.normal;
        elements.fixed = false;
        elements.inline = false;
        elements.labels = createLabels(document);
        elements.properties = createProperties(document);
        elements.reference = reference;
        return elements;
    }

    function createField(document: BaseMDMDocument, parent: IMDMObject, name: string, dataType: DataTypes, uuid?: string): Field {
        const field = createLabeledObject(document, parent, ObjectFlags.field, name, uuid) as Mutable<Field>;
        field.dataType = dataType;
        field.elements = createMDMElements(document, field, field);
        return field;
    }

    function createVariable(document: BaseMDMDocument, parent: IMDMObject, name: string, dataType: DataTypes, uuid?: string): Variable {
        const variable = createField(document, parent, name, dataType, uuid) as Mutable<Variable>;
        variable.objectFlag = ObjectFlags.variable;
        variable.helperFields = createTypes(variable);
        variable.baseName = name;
        variable.usageType = VariableUsageFlags.variable;
        variable.hasCaseData = true;
        variable.versioned = true;
        variable.sourceType = SourceTypes.none;
        return variable;
    }

    function createVariables(items?: readonly Variable[], children?: readonly Variable[]): Variables {
        const variables = new Map<string, Variable>(items ? getObjectArrayMapEntries(items, i => i.uuid.toLowerCase()) : []) as ReadonlyMap<string, Variable> as Mutable<Variables>;
        variables.objectFlag = ObjectFlags.variables;
        if (children) {
            variables.children = createVariables(children);
        }
        return variables;
    }

    function createMDMElementInstace(document: BaseMDMDocument, parent: IMDMObject, fullName?: string): MDMElementInstance {
        const instance = createObject(document, parent, ObjectFlags.elementInstance, "") as Mutable<MDMElementInstance>;
        instance.fullName = fullName;
        return instance;
    }

    function createMDMElementInstances(items?: readonly MDMElementInstance[], deleted?: readonly MDMElementInstance[]): MDMElementInstances {
        const instances = createMDMList(items, deleted) as Mutable<MDMElementInstances>;
        instances.objectFlag = ObjectFlags.elementInstances;
        return instances;
    }

    function createVariableInstance(parent: IMDMObject, variable: Variable, fullName?: string, uuid?: string): VariableInstance {
        const instance = createObject(parent.document, parent, ObjectFlags.variableInstance, variable.name, uuid) as Mutable<VariableInstance>;
        instance.objectFlag = ObjectFlags.variableInstance;
        instance.parent = parent;
        instance.uuid = uuid ?? getUuid();
        instance.fullName = fullName ?? variable.name;
        return instance;
    }

    function createClass(document: BaseMDMDocument, parent: IMDMObject, name?: string, id?: string): Class {
        const classObject = createLabeledObject(document, parent,ObjectFlags.class,  name ?? "@class", id) as Mutable<Class>;
        classObject.fields = createTypes(classObject);
        classObject.types = createTypes(classObject);
        classObject.helperFields = createTypes(classObject);
        classObject.fullName = name ? `${isMDMField(parent) && parent.iteratorType !== undefined ? `${parent.fullName}[..]` : parent.name}.${name}` : "";
        classObject.dataSourceProperties = createProperties(document);
        classObject.relativeName = "";
        classObject.items = createTypes(classObject);
        classObject.versioned = true;
        classObject.namespace = false;
        return classObject;
    }

    function createMDMField(document: BaseMDMDocument, parent: IMDMObject, name: string, dataType: DataTypes, uuid?: string): MDMField {
        const field = createField(document, parent, name, dataType, uuid) as Mutable<MDMField>;
        field.objectFlag = ObjectFlags.field;
        field.elements = createMDMElements(document, field, field);
        field.helperFields = createTypes(field);
        field.ranges = undefined;
        field.fullName = name;
        field.dataSourceProperties = undefined;
        field.variables = createVariables();
        field.expressions = undefined;
        field.fields = createTypes(field);
        field.isDerived = false;
        field.notes = createNotes();
        field.dbQuestionProperties = undefined;
        field.dbQuestionType = undefined;
        field.codes = undefined;
        field.dataType = dataType;
        field.minValue = undefined;
        field.maxValue = undefined;
        field.effectiveMinValue = undefined;
        field.effectiveMaxValue = undefined;
        field.usageType = VariableUsageFlags.variable;
        field.hasCaseData = true;
        field.versioned = true;
        field.iteratorType = undefined;
        field.lowerBound = undefined;
        field.upperBound = undefined;
        field.orientation = undefined;
        field.isGrid = false;
        field.expand = true;
        field.validation = undefined;
        field.precisions = undefined;
        field.rangeExpression = undefined;
        field.expression = undefined;
        field.axisExpression = undefined;
        field.class = createClass(document, field);
        field.defaultAnswer = undefined;
        field.initialAnswer = undefined;
        return field;
    }

    function createCompound(document: BaseMDMDocument, parent: IMDMObject, name: string, uuid?: string): Compound {
        const compound = createLabeledObject(document, parent, ObjectFlags.compound, name, uuid) as Mutable<Compound>;
        compound.fields = createTypes(compound);
        compound.types = createTypes(compound);
        compound.helperFields = createTypes(compound);
        compound.routing = undefined;
        compound.versioned = true;
        compound.namespace = false;
        compound.fullName = name;
        compound.orientation = undefined;
        compound.otherCategories = createCategories();
        compound.pages = createPages(compound, "@pages");
        compound.isValid = true;
        return compound;
    }

    function createPage(document: BaseMDMDocument, parent: IMDMObject, name: string, uuid?: string, fields?: readonly Field[]): Page {
        const page = createMDMList(fields) as Mutable<Page>;
        page.document = document;
        page.parent = parent;
        page.uuid = uuid ?? getUuid();
        page.name = name;
        page.fullName = name;
        page.labels = createLabels(document);
        page.relativeName = name;
        page.objectFlag = ObjectFlags.page;
        page.properties = createProperties(document);
        return page;
    }

    function createPages(parent: IMDMObject, name: string, uuid?: string, items?: readonly Page[], deleted?: readonly Page[]): Pages {
        const pages = createTypes(parent, uuid, getMDMObjectEntries(items)) as Mutable<Pages>;
        pages.objectFlag = ObjectFlags.pages;
        pages.name = name;
        pages.namespace = false;
        if (deleted) {
            pages.deleted = createTypes(parent, getUuid(), getMDMObjectEntries(deleted));
        }
        return pages;
    }

    function createAliasMap(alias?: readonly Alias[]): AliasMap {
        const map = createMDMList(alias) as Mutable<AliasMap>;
        map.objectFlag = ObjectFlags.aliasMap;
        return map;
    }

    function createAlias(document: BaseMDMDocument, items?: readonly SubAlias[]): Alias {
        const alias = createMDMList(items) as Mutable<Alias>;
        alias.properties = createProperties(document);
        alias.nativeValues = [];
        return alias;
    }

    function createScript(name: string, script: string, isDefault: boolean): Script {
        return { name, script, isDefault };
    }

    function createScriptType(type: string, context: string, interviewMode: InterviewModesFlags, useKeyCode = false, items?: readonly Script[]): ScriptType {
        const scriptType = createMDMList(items) as Mutable<ScriptType>;
        scriptType.type = type;
        scriptType.context = context;
        scriptType.interviewMode = interviewMode;
        scriptType.useKeyCode = useKeyCode;
        return scriptType;
    }

    function createRouting(context: string, interviewMode?: InterviewModesFlags, useKeyCode = false, items?: readonly RoutingItem[]): Routing {
        const routing = createMDMList(items) as Mutable<Routing>;
        routing.objectFlag = ObjectFlags.routing;
        routing.context = context;
        routing.interviewMode = interviewMode ?? InterviewModesFlags.empty;
        routing.useKeyCode = useKeyCode;
        return routing;
    }

    function createRoutings(document: BaseMDMDocument, name?: string, scripts?: Scripts, properties?: Properties, items?: readonly Routing[]): Routings {
        const routings = createMDMList(items) as Mutable<Routings>;
        routings.name = name;
        routings.scripts = scripts ?? createMDMList<ScriptType>();
        routings.properties = properties ?? createProperties(document);
        return routings;
    }

    function createBaseDocument(): BaseMDMDocument {
        const document = new (documentConstructor || (documentConstructor = objectAllocator.getBaseDocumentConstructor()))() as Mutable<BaseMDMDocument>;
        document.versions = createVersions(document, document);
        document.currentVersion = "";
        document.mdmVersion = _options.mdmVersion;
        document.pages = createPages(document, "@pages");
        document.createdByVersion = _options.mdmVersion;
        document.lastUpdatedByVersion = _options.mdmVersion;
        document.systemRouting = createRoutings(document);
        document.routings = createRoutings(document);
        document.categoryMap = createCategoryMap(document);
        document.aliasMap = createAliasMap();
        document.dataSources = createDataSources();
        document.languages = createLanguages(document, _options.defaultLanguage.longCode, _options.defaultLanguage.longCode);
        document.contexts = createContexts(document, _options.defaultContext, _options.defaultContext);
        document.labelTypes = createContexts(document, _options.defaultLabelType, _options.defaultLabelType);
        document.routingContexts = createContexts(document, _options.defaultRoutingContext, _options.defaultRoutingContext);
        document.scriptTypes = createContexts(document, _options.defaultScriptType, _options.defaultScriptType);
        return document;
    }

}